# Don't think I'll be winning any awards for AI :)

from rlpath import *
from rlcore import *
from erlglobals import *
from erlitem import *

from erlrays import *

from erlmaininput import ContinueOnSpace

import copy

BigBad = "Emperor engine" # hack!

AllDirections = [ (dx,dy) for dx in (-1,0,1) for dy in (-1,0,1) if dx or dy ]

class CannotGenerate: pass
class NonPC (Mobile):
	serializedAttributes = Mobile.serializedAttributes + [
		"timekeeper",
		"friendly",
		"ai",
		"paintHighlights",
		"game",
		"name",
		"inactivity",
		"beamAttack",
		"beamCooldown",
		"burstAttack",
		"burstCooldown"
	]
	def __init__(self, game, ai, level, position = None, friendly = False, meleeAttack = 5, inventory = None, beamAttack = None, burstAttack = None, *args, **kwargs):
		Mobile.__init__( self, *args, **kwargs )
		if not position:
			x, y = level.getRandomMovableTile( self )
		else:
			x, y = position
			if not self.mayMove( level.tiles[x][y] ):
				raise CannotGenerate()
		self.move( level, x , y )
		self.game = game
		self.meleeAttack = meleeAttack
		self.ai = ai
		self.friendly = friendly
		self.timekeeper = game.timekeeper
		self.timekeeper.nonplayers.append( self )
		self.paintHighlights = False
		self.beamAttack = beamAttack
		self.beamCooldown = 0
		self.burstAttack = burstAttack
		self.burstCooldown = 0
		if inventory:
			self.inventory = inventory # the original will be empty anyway
	def act(self):
		self.ai( self )
	def tick(self):
		self.inactivity += 1
		self.beamCooldown = max( self.beamCooldown - 1 , 0 )
		self.burstCooldown = max( self.burstCooldown - 1 , 0 )
		self.frozenCountdown = max( self.frozenCountdown - 1, 0 )
		self.hp = min( self.hp + 1, self.maxHp )
		pass
	def deathDrops(self):
		for item in self.inventory:
			item = item.take( item.amount )
			self.level.putItem( item, self.x, self.y )
		core = RuntimeItemUnique (
			Noun( short=("a", "technology core (%s)" % self.name, "technology cores (%s)" % self.name) ),
			Glyph( '#', (0,0,0) ),
			400,
			"a technology core (%s)" % self.name 
		)
		core.technologyCore = self.name
		self.level.putItem( core, self.x, self.y )
	def die(self, noDrops = False):
		# This is the one place where we don't need special effects
		# (blood and guts), since the opponents will generally be
		# robots that drop useful items as guts (nuts and bolts etc)
		if self.isVisible():
			self.game.interface.messages.add( "The %s is destroyed!" % (self.name) )
		self.timekeeper.nonplayers.remove( self )
		assert self.level.tiles[self.x][self.y].mobile == self
		if not noDrops:
			self.deathDrops()
		self.level.tiles[self.x][self.y].mobile = None
		if self.name == BigBad:
			def raisePlayerWon():
				raise PlayerWon()
			self.game.interface.messages.add( "<DARKBRASS>You have destroyed %s and won the game!</DARKBRASS> [press space to continue]" % self.name )
			ContinueOnSpace( self.game.interface, do = raisePlayerWon )
	def isVisible(self):
		return self.level == self.game.at.level and self.game.at.fov.isLit( self.x, self.y )
	def damageMessage(self, amount):
		if self.isVisible():
			self.game.interface.messages.add( "The %s takes <DARKBRASS>%d</DARKBRASS> damage!" % (self.name, amount ) )
	def visualMessage(self, msg):
		if self.isVisible():
			self.game.interface.messages.add( msg )
	def mayFireBeam(self):
		return self.beamAttack and self.beamCooldown <= 0
	def mayFireBurst(self):
		return self.burstAttack and self.burstCooldown <= 0
	def fireDirectionalBeam(self, direction):
		msg, cooldown, initialPower, effectClass, projectorClass = self.beamAttack
		self.beamCooldown = cooldown
		self.visualMessage( msg )
		effect = effectClass()
		effect.power = initialPower
		effectProjector = projectorClass( interface=self.game.interface, level =self.level, effect =effect, origin =(self.x,self.y), direction = direction )
		effectProjector.project()
	def getDirectionalBeamArea(self, direction):
		msg, cooldown, initialPower, effectClass, projectorClass = self.beamAttack
		effect = None
		effectProjector = projectorClass( interface=self.game.interface, level =self.level, effect =effect, origin =(self.x,self.y), direction = direction )
		return effectProjector.area
	def getBurstArea(self):
		msg, cooldown, initialPower, effectClass, projectorClass = self.burstAttack
		effect = None
		effectProjector = projectorClass( interface=self.game.interface, level =self.level, effect =effect, origin =(self.x,self.y) )
		return effectProjector.area
	def fireBurst(self, explode = False):
		msg, cooldown, initialPower, effectClass, projectorClass = self.burstAttack
		self.burstCooldown = cooldown
		self.visualMessage( msg )
		effect = effectClass()
		effect.power = initialPower
		effectProjector = projectorClass( interface=self.game.interface, level =self.level, effect =effect, origin =(self.x,self.y) )
		if explode:
			effectProjector.display()
			effectProjector.display()
		effectProjector.project()
		if explode:
			self.die( noDrops = True )
	def breakDoor(self, tile):
		assert tile.type == self.game.gtt.closedDoor
		self.visualMessage( "The %s destroys the door!" % self.name )
		tile.type = self.game.gtt.brokenDoor
	def openDoor(self, tile):
		assert tile.type == self.game.gtt.closedDoor
		tile.type = self.game.gtt.openDoor


def aiPassive( npc ):
	pass
isAllowedPointer( aiPassive )

def aiRandomWalk( npc ):
	ways = []
	for dx in (-1,0,1):
		for dy in (-1,0,1):
			if not (dx or dy): continue
			if npc.mayMove( npc.getTileRelative( (dx,dy) ) ):
				ways.append( (dx,dy) )
	if ways:
		npc.moveRelative( random.choice( ways ) )
isAllowedPointer( aiRandomWalk )


def aiHaywireDirectionalBeam( npc ):
	if npc.level != npc.game.at.level: return aiRandomWalk(npc)
	if npc.mayFireBeam():
		direction = random.choice( [ (dx,dy) for dx in (-1,0,1) for dy in (-1,0,1) if dx or dy ] )
		npc.fireDirectionalBeam( direction )
	else:
		aiRandomWalk( npc )
isAllowedPointer( aiHaywireDirectionalBeam )

def getNextShooterShotReckless( npc ):
	if not npc.mayFireBeam(): return None
	goodDirs = []
	for dir in AllDirections:
		hitMobs = [ npc.level.tiles[x][y].mobile for x,y in npc.getDirectionalBeamArea( dir ) if npc.level.tiles[x][y].mobile ]
		if npc.game.at in hitMobs:
			goodDirs.append( dir )
	if not goodDirs: return None
	return random.choice( goodDirs )
	
def shouldBurstReckless( npc ):
	if not npc.mayFireBurst(): return False
	hitMobs = [ npc.level.tiles[x][y].mobile for x,y in npc.getBurstArea() if npc.level.tiles[x][y].mobile ]
	return npc.game.at in hitMobs

def aiRecklessDirectionalBeam( npc ):
	if npc.level != npc.game.at.level: return aiRandomWalk( npc )
	dir = getNextShooterShotReckless( npc )
	if dir:
		npc.fireDirectionalBeam( dir )
	else:
		aiRandomWalk( npc )
isAllowedPointer( aiRecklessDirectionalBeam )
		

class PathEvaluator:
	def __init__(self, mover, level):
		self.mover = mover
		self.level = level
	def __call__(self, path):
		rv = 0
		for x, y in path:
			tile = self.level.tiles[x][y]
			if not tile.type.traversibility in self.mover.mobility:
				return blocked,0
			if tile.mobile:
				rv += getattr( tile.mobile, "inactivity", 0 )
				# Kudos for this to whoever suggested it on rgrd
			rv += 1
		return rv, 0

class PathEvaluatorLinearTTD: # tile type dictionary
	def __init__(self, ttd, level):
		self.ttd = ttd
		self.level = level
	def __call__(self, path):
		rv = 0
		for x, y in path:
			tile = self.level.tiles[x][y]
			if not self.ttd.has_key( tile.type ):
				return blocked, 0
			else:
				rv += self.ttd[ tile.type ]
			if tile.mobile:
				rv += getattr( tile.mobile, "inactivity", 0 )
				# Kudos for this to whoever suggested it on rgrd
		return rv, 0

#class AiPathfinder:
#	serializedAttributes = [
#		"lastPath"
#	]
#	def __init__(self):
#		self.lastPath = None
#	def wantRefreshPath(self):
#		limit = 10
#		chance = 0.1
#		return not self.lastPath or len( self.lastPath ) < limit or random.random() < chance
#	def getPath(self):
#		if not self.wantRefreshPath(): return self.lastPath
#	def moveOn(self)
#		pass
#supportsSerialization( AiPathfinder )

		
def aiPlayerSeeker(npc):
	if npc.level != npc.game.at.level: return aiRandomWalk(npc)
	if npc.isAdjacent( npc.game.at ): return npc.attackMelee( npc.game.at )
	p = npc.game.at.x, npc.game.at.y

	pf = Pathfinder( goal = goalPoint( p ), evaluatePath = PathEvaluator( npc, npc.level ), heuristic = heuristicPointRoguelike(p) )
	pf.addStart( (npc.x, npc.y) )
	path = pf.findPath()
	if path and len(path) > 1:
		print "made path", len(path)
		x, y = path[1]
		if npc.mayMove( npc.level.tiles[x][y] ):
			npc.move( npc.level, x, y )
isAllowedPointer( aiPlayerSeeker )

def getNextSeekerStep(npc, alternate = False):
	if not alternate:
		l = [ (npc.level.tiles[npc.x+dx][npc.y+dy].pcDist,npc.x+dx,npc.y+dy) for dx,dy in AllDirections if npc.level.tiles[npc.x+dx][npc.y+dy].pcDist != None ]
	else:
		l = [ (npc.level.tiles[npc.x+dx][npc.y+dy].pcDistDalek,npc.x+dx,npc.y+dy) for dx,dy in AllDirections if npc.level.tiles[npc.x+dx][npc.y+dy].pcDist != None ]
	if not l: return aiRandomWalk( npc ) #what's happening? dunno
	l.sort()
	l = [ elt for elt in l if elt[0] == l[0][0] ]
	d, x, y = random.choice( l )
	return x,y

def aiDoorbreakerBrute(npc):
	if npc.level != npc.game.at.level: return aiRandomWalk( npc )
	if npc.isAdjacent( npc.game.at ): return npc.attackMelee( npc.game.at )
	x, y = getNextSeekerStep( npc )
	if npc.mayMove( npc.level.tiles[x][y] ):
		npc.move( npc.level, x, y )
	elif npc.level.tiles[x][y].type == npc.game.gtt.closedDoor:
		npc.breakDoor( npc.level.tiles[x][y] )
	else:
		return aiRandomWalk(npc) # I just don't know what to do with myself..
isAllowedPointer( aiDoorbreakerBrute )

def aiEmperor(npc):
	if npc.level != npc.game.at.level: return aiRandomWalk( npc )
	if npc.isAdjacent( npc.game.at ): return npc.attackMelee( npc.game.at )
	dir = getNextShooterShotReckless( npc )
	if dir:
		npc.fireDirectionalBeam( dir )
		return
	x, y = getNextSeekerStep( npc )
	if npc.mayMove( npc.level.tiles[x][y] ):
		npc.move( npc.level, x, y )
	elif npc.level.tiles[x][y].type == npc.game.gtt.closedDoor:
		npc.breakDoor( npc.level.tiles[x][y] )
	else:
		return aiRandomWalk(npc) # I just don't know what to do with myself..
isAllowedPointer( aiEmperor )

def aiDoorOpenerBeamDirectionalReckless(npc):
	if npc.level != npc.game.at.level: return aiRandomWalk( npc )
	dir = getNextShooterShotReckless( npc )
	if dir:
		npc.fireDirectionalBeam( dir )
		return
	x, y = getNextSeekerStep(npc)
	if npc.mayMove( npc.level.tiles[x][y] ):
		npc.move( npc.level, x, y )
	elif npc.level.tiles[x][y].type == npc.game.gtt.closedDoor:
		npc.openDoor( npc.level.tiles[x][y] )
	else:
		return aiRandomWalk(npc) # I just don't know what to do with myself..
isAllowedPointer( aiDoorOpenerBeamDirectionalReckless )
	
class aiRandomWalkUntilPlayerClose:
	serializedAttributes = [
		"radius"
	]
	def __init__(self, radius):
		self.radius = radius
	def __call__(self, npc):
		if npc.level != npc.game.at.level: return aiRandomWalk( npc )
		if npc.isAdjacent( npc.game.at ): return npc.attackMelee( npc.game.at )
		if npc.level.tiles[npc.x][npc.y].pcDist > self.radius:
			return aiRandomWalk( npc )
		else:
			x, y = getNextSeekerStep( npc )
			if npc.mayMove( npc.level.tiles[x][y] ):
				npc.move( npc.level, x, y )
			else:
				return aiRandomWalk( npc )
supportsSerialization( aiRandomWalkUntilPlayerClose )

class RobotData:
	def __init__(self, name, glyph, resourcesAmount, materialsAmount, materialsType, maxHp, melee = None, ai = aiPassive, specialDrops = [], beam = None, fuelType = None, fuelAmount = None, burst = None):
		self.name = name
		self.glyph = glyph
		self.melee = melee
			# e.g. ("attacks you with its buzzsaw", "chops through the air with its buzzsaw", 20 )
			# 	melee attacks are the average of two random.randint(1,n)
		self.ai = ai
		self.resourcesAmount = resourcesAmount
		self.materialsAmount = materialsAmount
		self.materialsType = materialsType
		self.specialDrops = specialDrops
		self.maxHp = maxHp
		self.beam = beam
		self.fuelType = fuelType
		self.fuelAmount = fuelAmount
		self.burst = burst
	def addRandomResources(self, amount, loot):
		for i in range(amount):
			loot.add( random.choice( ItemTable.RandomResources ).spawn() )
	def spawn(self, game, level, position = None):
		loot = Inventory()
		self.addRandomResources( plentyMultiplier * randomGaussModify( self.resourcesAmount ), loot )
		loot.add( self.materialsType.spawn( randomGaussModify( plentyMultiplier * self.materialsAmount ) ) )
		for item in self.specialDrops:
			loot.add( item.spawn() )

		if self.fuelType and self.fuelAmount:
			loot.add( self.fuelType.spawn( randomGaussModify( plentyMultiplier * self.fuelAmount ) ) )

		return NonPC(
			game = game,
			level = level,
			name = self.name,
			ai = self.ai,
			meleeAttack = self.melee,
			inventory = loot,
			position = position,
			mobility = [ "land", "open-door" ],
			glyph = self.glyph,
			maxHp = self.maxHp,
			beamAttack = self.beam,
			burstAttack = self.burst
		)

def aiFreezeTurtle( npc ):
	if random.random() > 0.5: return # turtle, slow, geddit?
	if npc.level != npc.game.at.level: return aiRandomWalk( npc )
	if npc.isAdjacent( npc.game.at ): return npc.attackMelee( npc.game.at )
	x, y = getNextSeekerStep(npc)
	if npc.game.at.frozenCountdown == 0 and shouldBurstReckless( npc ):
		return npc.fireBurst()
	if npc.level.tiles[x][y].pcDist < 4 and npc.mayMove( npc.level.tiles[x][y] ):
		npc.move( npc.level, x, y )
	else:
		aiRandomWalk( npc )
isAllowedPointer( aiFreezeTurtle )

def aiExploder( npc ):
	if npc.isAdjacent( npc.game.at ): return npc.fireBurst( explode = True )
	x, y = getNextSeekerStep(npc, alternate=True)
	if npc.mayMove( npc.level.tiles[x][y] ):
		npc.move( npc.level, x, y )
	else:
		aiRandomWalk( npc )
isAllowedPointer( aiExploder )

Robots = { # by level. data is: name, melee weapon (name,str)
	1: [
		RobotData(
			name = "patrol robot",
			glyph = Glyph( 'i', (255,255,255) ),
			beam = (
				"The patrol robot fires a beam!",
				4, #cooldown,
				1.0, # initial power
				HeatEffect,
				EffectProjectorLine,
			),
			ai = aiRecklessDirectionalBeam,
			resourcesAmount = 15 ,
			materialsAmount = 600.0,
			materialsType = ItemTable.Iron,
			maxHp = 100,
			fuelType = ItemTable.Coal,
			fuelAmount = 500
		),
		RobotData(
			name = "drone automaton",
			glyph = Glyph( 'd', (255,255,255) ),
			melee = ( "The drone automaton pokes %s with its sharpened arm!",
				  "The drone automaton tries to attack %s, but misses.",
				  0.5, 10 ),
			ai = aiRandomWalkUntilPlayerClose( radius = 5 ),
			resourcesAmount = 10 ,
			materialsAmount = 400.0,
			materialsType = ItemTable.Brass,
			maxHp = 40,
			fuelType = ItemTable.Coal,
			fuelAmount = 500
		),
	],
	2: [
		RobotData(
			name = "clockwork brute",
			glyph = Glyph( 'b', (255,255,255) ),
			melee = ( "The clockwork brute attacks %s with its metallic fist!",
				  "The clockwork brute attack %s, but misses.",
				  0.7, 12 ),
			ai = aiDoorbreakerBrute,
			resourcesAmount = 25 ,
			materialsAmount = 1000.0,
			materialsType = ItemTable.Brass,
			maxHp = 150,
			fuelType = None, fuelAmount = None,
		),
		RobotData(
			name = "brass spider",
			glyph = Glyph( 'M', (255,255,255) ),
			beam = (
				"The brass spider fires a beam!",
				4, #cooldown,
				2.0, # initial power
				HeatEffect,
				EffectProjectorWideAngleStandard,
			),
			ai = aiRecklessDirectionalBeam,
			resourcesAmount = 25 ,
			materialsAmount = 1000.0,
			materialsType = ItemTable.Brass,
			maxHp = 150,
			fuelType = ItemTable.Coal,
			fuelAmount = 1000
		),
	],
	3: [
		RobotData(
			name = "guardian drone",
			glyph = Glyph( 'U', (255,255,255) ),
			beam = (
				"The guardian drone launches a ball of energy!",
				4,
				1.0,
				HeatEffect,
				EffectProjectorBouncerBallLong,
			),
			ai = aiRecklessDirectionalBeam,
			resourcesAmount = 20,
			materialsAmount = 500.0,
			materialsType = ItemTable.Brass,
			maxHp = 100,
			fuelType = ItemTable.Coal,
			fuelAmount = 400
		),
		RobotData(
			name = "iron turtle",
			glyph = Glyph( 'o', (255,255,255) ),
			melee = ( "The iron turtle bites %s!",
				  "The iron turtle tries to bite %s, but misses.",
				  0.75, 5 ),
			burst = (
				"A blue burst emanates from the iron turtle!",
				4, #cooldown,
				1.0, # initial power
				FreezeEffect,
				EffectProjectorRadialShort,
			),
			ai = aiFreezeTurtle,
			resourcesAmount = 24 ,
			materialsAmount = 200.0,
			materialsType = ItemTable.Iron,
			maxHp = 40,
			fuelType = ItemTable.Coal,
			fuelAmount = 400
		),
	],
	4: [
		RobotData(
			name = "golden android",
			glyph = Glyph( '@', (0xff,0xd7,0x00) ),
			beam = (
				"The golden android projects beams from its eyes!",
				4, #cooldown,
				4.0, # initial power
				HeatEffect,
				EffectProjectorLine,
			),
			ai = aiDoorOpenerBeamDirectionalReckless,
			resourcesAmount = 35 ,
			materialsAmount = 1000.0,
			materialsType = ItemTable.Gold,
			maxHp = 150,
			fuelType = ItemTable.Coal, fuelAmount = 500,
		),
		RobotData(
			name = "nitroglycerin bee",
			glyph = Glyph( 'w', (0xff,0xd7,0x00) ),
			ai = aiExploder,
			burst = (
				"The nitroglycerin bee explodes!",
				1, # rather irrelevant..
				8.0,
				HeatEffect,
				EffectProjectorRadialShort
			),
			resourcesAmount = 30 ,
			materialsAmount = 100.0,
			materialsType = ItemTable.Iron,
			maxHp = 20,
			fuelType = ItemTable.Coal, fuelAmount = 100,
			specialDrops = [ ItemTable.Explosive ]
		),
	],
	5: [
		RobotData( #unfinished
			name = BigBad,
			glyph = Glyph( '&', (255,255,255) ),
			melee = ( "The Emperor engine slashes %s with its sharp blades!",
				  "The Emperor engine slashes through the air with its sharp blades, but misses %s.",
				  0.75, 45 ),
			beam = (
				"The Emperor engine lets loose a stream of destructive energy!",
				4,
				5.0,
				HeatEffect,
				EffectProjectorWideAngleStandard,
			),
			burst = (
				"The Emperor engine lets loose a burst of destructive energy!",
				4,
				1.0,
				HeatEffect,
				EffectProjectorBouncerBallLong,
			),
			ai = aiEmperor,
			resourcesAmount = 1200,
			materialsAmount = 20000.0,
			materialsType = ItemTable.Iron,
			maxHp = 1000,
			fuelType = ItemTable.Coal,
			fuelAmount = 40000
				# after this dude is defeated, the game is essentially won.
				# so there's no harm in giving it unbalancing amounts of resources.
		),
	]
}
